<?php
/** 
 * 
 *
 * @package api
 * @subpackage plugins
 * @author $Author$
 * @version $Id: PluginManager.php 14 2006-11-28 22:05:19Z rdff3 $
 */
/**
 *
 */
require_once( "lang/Singleton.php" );
require_once( "lang/InvalidInheritanceException.php" );
require_once( "lang/IllegalArgumentException.php" );
require_once( "lang/IndexOutOfBoundsException.php" );

/**
 * Main plugin facade class.
 *
 * All plugin activites that should be used by programs are contained in this class.  Programmers may call the public static methods 
 * within their code to create a plugin system.
 *
 * Plugins should exist as folders within a main plugin folder.  Each plugin folder must contain a plugins.xml file with following format.
 * <code>
 *
 * 	<?xml version="1.0" encoding="iso-8859-1"?>
 *	<plugins>
 *		<plugin name="plugin_name" filename="relative/path/to/file.php">
 *			<hook name="program_hook_name">
 *				<function priority="1" name="function_name1" />
 *				<function priority="10" name="function_name2" />
 *				...
 *			</hook>
 *			...
 *		</plugin>
 *		...
 *	</plugins>
 *
 * </code>
 *
 * This format tells the plugin manager how to load the plugin into its data structures.  All fields and attributes are required. If some
 * fields or attributes are missing, there are no gaurentee's as to the plugin managers behavior.
 *
 * Applications wishing to load plugins should use the loadPlugins function.
 *
 * 		PluginManager::loadPlugins("path/to/plugins/dir");
 *
 * This class is part of the PHP Plugin Architecture
 *
 * @package api
 * @subpackage plugins
 * @author $Author$
 * @version $Id: PluginManager.php 14 2006-11-28 22:05:19Z rdff3 $
 */
class PluginManager {
	private static $plugin_registry;
	private static $registered_plugins;
	private static $plugin_path;
	
	
	/**
	 * Loads all plugins contained in the $path_to_plugins_dir as plugins.
	 *
	 * The directory specified should contain folders representing plugins.  Each plugin folder should have a plugins.xml file
	 * that tells the plugin manager how the plugin should be loaded.  The details of this specification or listed in this class's 
	 * description.
	 *
	 * 
	 * 
	 * @param string $path_to_plugins_dir the absolute or relative path to the directory containing plugins.
	 * @throws Exception - if a plugins.xml file is not in the correct format.
	 */
	public static function loadPlugins( $path_to_plugins_dir ) {
		self::$plugin_registry = PluginRegistry::instance();
		self::$registered_plugins = array();
		self::$plugin_path = $path_to_plugins_dir;
		
	
		$dh = opendir( "$path_to_plugins_dir" . "/" );
		while( $file = readdir( $dh ) ) {
			
			if ($file == "." or $file == "..")
				continue;
			
			if (is_dir( "$path_to_plugins_dir/$file" )) {
				$plugin_file = "$path_to_plugins_dir/$file/plugins.xml";
			
				if (file_exists( $plugin_file )) {
					try {
						self::registerPlugin( $plugin_file );
					}
					catch (Exception $e) {
						throw new Exception("$path_to_plugins_dir/$file/plugins.xml: " . $e->getMessage() );
					}
				}
			}
			
		}
	}
	
	/**
	 * Loads a plugin from a descriptive xml file.
	 *
	 * Parses the $plugin_file_name as an xml file and registers each function as belonging to specific program hooks.
	 *
	 *
	 * @param string $plugin_file_name the xml file from which to load this plugin.
	 * @throws Exception - if the $plugin_file_name was not in a valid xml format.
	 */
	private static function registerPlugin( $plugin_file_name ) {
		
		if (!$plugins = SimpleXml_load_file( $plugin_file_name ))
			throw new Exception( "plugin file is not in a valid xml format." ); 
		
		foreach( $plugins as $plugin ) {
			$plugin_name 		= (string) $plugin['name'];
			$plugin_filename 	= self::$plugin_path . "/" . (string) $plugin['filename'];
			foreach( $plugin as $hook) {
				$hook_name 		= (string) $hook['name'];
				foreach( $hook as $function ) {
					$priority 	= (int) $function['priority'];
					$func_name 	= (string) $function['name'];
					
					self::add_function($hook_name, $func_name, $plugin_filename, $priority );
				}
			}
			self::$registered_plugins = array_merge( self::$registered_plugins, array( $plugin_name ) );
		}
		
	}
	
	public static function getRegisteredPlugins() {
		return self::$registered_plugins;
	}
	
	public static function pluginExists( $plugin_name ) {
		return in_array( $plugin_name, self::$registered_plugins );
	}
	
	
	private static function add_function($hook_name, $function_to_add, $plugin_filename, $priority = 10) {
		if ( self::$plugin_registry->getFunctions($hook_name, $priority) ) {
			foreach( self::$plugin_registry->getFunctions($hook_name, $priority) as $filter) {
				if ( $filter->getName() == $function_to_add ) 
					return false;
			}
		}
		$hook_function = new PluginFunction( $plugin_filename, $function_to_add );
		self::$plugin_registry->addFunction($hook_name, $priority, $hook_function );
		return true;
	}
	
	private static function remove_function($hook_name, $function_to_remove, $priority = 10) {
		$toret = false;
		if ( self::$plugin_registry->getFunctions($hook_name, $priority) ) {
			foreach( self::$plugin_registry->getFunctions($hook_name, $priority) as $function) {			
				if ( $function->getName() == $function_to_remove ) {
					$toret = self::$plugin_registry->removeFunction( $hook_name, $priority, $function );
				}
			}
		}
		return $toret;
	}
	
	public static function do_action($hook_name, $arg = '') {	
		$extra_args = array_slice(func_get_args(), 2);
		$args = array_merge(array($arg), $extra_args);
		if ( is_null(self::$plugin_registry->getHookName( $hook_name ) ) ) {
			return;
		}
		else {
			self::$plugin_registry->prioritySort( $hook_name );
		}
		
		foreach (self::$plugin_registry->getHookName($hook_name) as $priority => $functions) {
			if ( !is_null($functions) ) {
				foreach($functions as $function) {
					
					$str = $function->invoke( $args );
				}
			}
		}
	}



	public static function apply_filters($hook_name, $string) {	
		if ( is_null(self::$plugin_registry->getHookName($hook_name)) ) {
			return $string;
		}
		else {
			self::$plugin_registry->prioritySort($hook_name);
		}
		$args = array_slice(func_get_args(), 2);
		$all_args = array_merge(array($string), $args);
				
		foreach (self::$plugin_registry->getHookName($hook_name) as $priority => $functions) {
			if ( !is_null($functions) ) {
				foreach($functions as $function) {
					$all_args = array_merge(array($string), $args);
					$string = $function->invoke( $all_args );
				}
			}
		}
		return $string;
	}
	
	private static function dump() {
		self::$plugin_registry->dump();
	}
}



/**
 * Plugin Registry class
 *
 * Allows for the storage of plugin functions that are associated with certain program tags.  This implementation allows for 10 different
 * levels of priority for each hook_name.
 *
 * This class is part of the PHP Plugin Architecture
 * 
 * @package api
 * @subpackage plugins
 * @author $Author$
 * @version $Id: PluginManager.php 14 2006-11-28 22:05:19Z rdff3 $
 */
class PluginRegistry extends Singleton {
	
	/**
	 * Internally stores the list of program tags along with the priorities of functions that will be executed with that hook_name.
	 */
	private $plugins = array();

	/**
	 * Internally stores this singleton instance of the class.
	 */
    private static $instance;
	
	/**
	 * 
	 */
	static function instance() {
        if ( ! self::$instance ) { self::$instance = new self(); }
        return self::$instance;
    }
	
	/**
	 * Gets the currently declared list of functions that are associated with a specific program hook_name.  
	 *
	 * It also returns the unsorted priorities associated with each function.  If no functions are currently associated with this
	 * hook_name, will return null.
	 *
	 * @param string $hook_name The specified hook_name for which to grab the currently declared list of functions.
	 * @return array the list of functions associated with this hook_name, null otherwise.
	 */
	public function getHookName( $hook_name ) {
		if (isset( $this->plugins[$hook_name] ))
			return $this->plugins[$hook_name];
		else
			return null;
	}
	
	/**
	 * Gets the current list of functions that are associated with a specific program hook_name and priority.  
	 *
	 * If no functions are currently associated with this hook_name/priority, will return null.
	 *
	 * @param string $hook_name The specified hook_name for which to grab the currently declared list of functions.
	 * @param string $priority The specifed priority for which to grab the curretly declared list of functions.
	 * @return array the list of functions associated with this hook_name/priority, null otherwise.
	 */
	public function getFunctions( $hook_name, $priority ) {
		if (isset( $this->plugins[$hook_name][$priority] ))
			return $this->plugins[$hook_name][$priority];
		else
			return null;
	}
	
	/**
	 * Adds a specific function to call for a hook_name/priority combo.
	 *
	 * Adds a specific function to call for a hook_name/priority combo.  This implementation will only accept priority levels 
	 * ranging inclusive from 1-10.
	 *
	 * Note: This implementation does allow for multiple functions to be attached to the same priority level for a specified 
	 * hook_name, but makes no garuntee about the order of execution.  If execution order of plugins are a concern, make sure to 
	 * register each of them at a different priority level so they are executed in the correct order.
	 *
	 * @param string $hook_name The hook_name for which to assign this function.
	 * @param string $priority The priority for which to assign this function.
	 * @param array $function_data The data associated with this function.
	 * @throws IndexOutOfBoundsException - if the priority is not inclusivly between 1 and 10.
	 */
	public function addFunction( $hook_name, $priority, PluginFunction $function ) {
		if (1 > $priority or $priority > 10)
			throw new IndexOutOfBoundsException("A plugin function's priority must be inclusively between 1 and 10");
	
		if (isset( $this->plugins[$hook_name][$priority] ))
			$this->plugins[$hook_name][$priority] = array_merge( $this->plugins[$hook_name][$priority], array($function));
		else
			$this->plugins[$hook_name][$priority] = array($function);
	} 
	
	public function removeFunction( $hook_name, $priority, PluginFunction $function ) {
		if (!isset( $this->plugins[$hook_name][$priority] ))
			return false;
		
		foreach ( $this->plugins[$hook_name][$priority] as $key => $registered_function ) {
			if ($registered_function->equals($function))
				unset( $this->plugins[$hook_name][$priority][$key] );
		}
		if (count($this->plugins[$hook_name][$priority]) == 0)
			unset( $this->plugins[$hook_name][$priority] );
		if (count($this->plugins[$hook_name]) == 0 )
			unset( $this->plugins[$hook_name] );
	}
	
	/**
	 * Sorts the list of functions associated with a hook_name according to their priority.
	 *
	 * @returns bool returns the php convention of ksort
	 */
	public function prioritySort( $hook_name ) {
		return ksort($this->plugins[$hook_name]);
	}

	/**
	 * Spits out the whole internally stored plugins array for diagnosis.
	 *
	 * @internal
	 */
	public function dump() {
		echo "<pre>";
		print_r($this->plugins);
		echo "</pre>";
	}
}


/** 
 * Internal class used for encapselating a plugin function.
 *
 * 
 *
 * This class is part of the PHP Plugin Architecture
 *
 * @todo - implement a caching medium for loading files on invocation.
 * @internal
 * @package api
 * @subpackage plugins
 * @author $Author$
 * @version $Id: PluginManager.php 14 2006-11-28 22:05:19Z rdff3 $
 */
class PluginFunction {
	private $file_path;
	private $name;
	
	public function __construct( $file_path, $name ) {
		$this->setFilePath( $file_path );
		$this->setName( $name );
	}
	
	public function setFilePath( $file_path ) {
		if ($file_path == "")
			throw new IllegalArgumentException("A plugin function must have a valid file path.");
		$this->file_path = $file_path;
	}
	
	public function setName( $name ) {
		if ($name == "")
			throw new IllegalArgumentException("A plugin function must have a valid name.");
		$this->name = $name;
	}
	
	
	public function getName() {
		return $this->name;
	}
	
	public function getFilePath() {
		return $this->file_path;
	}
	
	public function equals( PluginFunction $o ) {
		if ($o === $this)
			return true;
		else if ($o->getName() == $this->getName() and $o->getFilePath() == $this->getFilePath() )
			return true;
		else
			return false;
	}
	
	public function invoke( $args ) {
		include_once( $this->getFilePath() );
		
		$func = new ReflectionFunction( $this->getName() );
		return $func->invokeArgs( $args );
	} 
}


?>